package se.sll.itintegration.catalogue.capacity.service;

import com.codahale.metrics.JmxReporter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;
import lombok.extern.slf4j.Slf4j;
import se.sll.itintegration.catalogue.capacity.ejb.CapacityEjb;
import se.sll.itintegration.catalogue.capacity.ejb.NotificationSupportEjb;
import se.sll.itintegration.catalogue.capacity.entity.CapacityEntity;
import se.sll.itintegration.catalogue.capacity.entity.CareUnitReferenceEntity;
import se.sll.itintegration.catalogue.capacity.entity.ProductReferenceEntity;
import se.sll.service.catalogue.capacity.api.model.*;
import se.sll.service.catalogue.capacity.api.resource.CapacityService;
import se.sll.service.catalogue.common.ISO8601;
import se.sll.service.catalogue.common.exception.ServiceCatalogueException;
import se.sll.service.catalogue.common.exception.ServiceErrorCode;
import se.sll.service.catalogue.common.exception.ValidationException;
import se.sll.service.catalogue.common.validation.FacadeRESTValidator;

import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.validation.Validator;
import javax.ws.rs.PathParam;
import java.sql.Timestamp;
import java.util.*;

/**
 * Rest implementation for the Capacity services defined in the from RAML generated
 * JAX-RS interface.
 *
 * @see se.sll.service.catalogue.capacity.api.resource.CapacityService
 */
@Slf4j
@Stateless
public class CapacityFacadeREST implements CapacityService {

    @Inject
    private CapacityEjb capacityEjb;
    @Inject
    private NotificationSupportEjb notificationSupportEjb;
    @Inject
    private Validator beanValidator;

    private final MetricRegistry metrics = new MetricRegistry();
    private final Timer responses = metrics.timer("capacity");
    private final JmxReporter reporter;
    private static FacadeRESTValidator validator;

    /**
     *
     */
    public CapacityFacadeREST() {
        reporter = JmxReporter.forRegistry(metrics).build();
        reporter.start();
        validator = new FacadeRESTValidator();
    }

    @Override
    public GetCapacityResponse getCapacity() throws Exception {
        log.debug("Capacity2FacadeREST.list() ENTER");

        List<CareUnitRefList> result = null;
        try {
            result = copyCareUnitFromEntityToModel(capacityEjb.findAll());

            log.debug("Found {} Offers", new Object[]{result.size()});
        } catch (RuntimeException e) {
            e.printStackTrace();
            log.error("Capacity2FacadeREST.list() Caught RuntimeException: " + e.getMessage());
            throw new ServiceCatalogueException();
        }

        log.debug("Capacity2FacadeREST.list() EXIT");
        return GetCapacityResponse.jsonOK(result);
    }

    @Override
    public PostCapacityResponse postCapacity(CareUnit entity) throws Exception {
        log.debug("create: ", entity);
        final Timer.Context context = responses.time();
        try {
            validator.validate(entity, beanValidator);

            CareUnitReferenceEntity cre = this.getCareUnitReference(entity.getHsaID());
            ProductReferenceEntity pre = this.getProductReferenceEntity(cre, entity.getProduct());

            CapacityEntity ce = new CapacityEntity();
            ce.setCode(entity.getCode());
            ce.setCodeSystem(entity.getCodeSystem());
            ce.setValue(entity.getValue());
            ce.setCreatedByHsaID("CREATED");
            ce.setSourceSystemHsaID("SOURCE");
            Date date = new Date();
            log.info("HSA: " + entity.getHsaID() + ", validTo: " + entity.getValidTo()
                            + ", formatted: " + ISO8601.formatTime(entity.getValidTo())
            );
            ce.setCreationTime(new Timestamp(date.getTime()));
            ce.setValidTo(entity.getValidTo());
            //ce.setProduct(pre);
            if (pre.getCapacity() == null) {
                pre.setCapacity(new HashSet<CapacityEntity>());
            }
            try {
//                ce = capacityEjb.update(ce);
//                pre.setHsaID(cre);

                pre.getCapacity().add(ce);
                pre = capacityEjb.merge(pre);
            } catch (RuntimeException ex) {
                log.error(ex.getMessage(), ex);
                throw new ServiceCatalogueException();
            }

        } finally {
            context.stop();
        }

        return PostCapacityResponse.created();
    }

    @Override
    public PutCapacityResponse putCapacity(CareUnit entity) throws Exception {
        log.debug("update: ", entity);
        final Timer.Context context = responses.time();

        validator.validate(entity, beanValidator);

        try {
            CareUnitReferenceEntity existingCode = null;
            try {
                log.debug(entity.getHsaID() + ", " + entity.getProduct() + ", " + entity.getCode());
                existingCode = capacityEjb.findCapacity(entity.getHsaID(), entity.getProduct(), entity.getCode());
            } catch (RuntimeException ex) {
                log.error("Unable to get", ex);
                throw new ServiceCatalogueException();
            }

            CareUnitReferenceEntity cre = this.getCareUnitReference(entity.getHsaID());
            ProductReferenceEntity pre = this.getProductReferenceEntity(cre, entity.getProduct());

            try {
                if (existingCode != null) {
                    for (CapacityEntity e : pre.getCapacity()) {
                        if (e.getCode().equals(entity.getCode())) {
                            e.setValidTo(entity.getValidTo());
                            e.setValue(entity.getValue());
                            capacityEjb.mergeCapacity(e);
                        }
                    }
                } else {
                    CapacityEntity ce = new CapacityEntity();
                    ce.setCode(entity.getCode());
                    ce.setCodeSystem(entity.getCodeSystem());
                    ce.setValue(entity.getValue());
                    ce.setCreatedByHsaID("CREATED");
                    ce.setSourceSystemHsaID("SOURCE");
                    Date date = new Date();
                    ce.setCreationTime(new Timestamp(date.getTime()));
                    ce.setValidTo(entity.getValidTo());
                    pre.getCapacity().add(ce);
                }
                capacityEjb.merge(pre);
            } catch (RuntimeException ex) {
                log.error(ex.getMessage());
                throw new ServiceCatalogueException();
            }
        } finally {
            context.stop();
        }

        return PutCapacityResponse.oK();
    }

    @Override
    public PostCapacityByunitidsResponse postCapacityByunitids(UnitList entity) throws Exception {
        log.debug("CapacityFacadeREST.listByUnitIds(): ENTER");
        long start = System.currentTimeMillis();

        entity.getUnits();

        List<CareUnitRefList> result = null;
        if (entity == null || entity.getUnits().isEmpty()) {
            throw new ValidationException(ServiceErrorCode.UNIT_LIST_INVALID_EMPTY);
        }

        try {
            Set<String> input = new HashSet<>();
            for (Unit unit : entity.getUnits()) {
                if (unit.getUnitId() != null) {
                    input.add(unit.getUnitId());
                }
            }
            result = copyCareUnitFromEntityToModel(capacityEjb.findByUnits(input));
        } catch (RuntimeException e) {
            log.error("CapacityFacadeREST.listByUnitIds() Caught Exception: Type: {}, Message: {} ", new Object[]{e.getClass().getName(), e.getMessage()});
            throw new ServiceCatalogueException();
        }
        // Copy to external model
        log.debug("CapacityFacadeREST.listByUnitIds(time = {} ms): EXIT", (System.currentTimeMillis() - start));
        return PostCapacityByunitidsResponse.jsonOK(result);
    }

    @Override
    public GetCapacityByHsaIdResponse getCapacityByHsaId(@PathParam("hsaId") String hsaId) throws Exception {
        log.debug("Capacity2FacadeREST.getCapacityByHsaId() ENTER");

        List<CareUnitRefList> result = null;
        try {
            result = copyCareUnitFromEntityToModel(capacityEjb.findByHsaId(hsaId));

            log.debug("Found {} Offers", new Object[]{result.size()});
        } catch (RuntimeException e) {
            e.printStackTrace();
            log.error("Capacity2FacadeREST.getCapacityByHsaId() Caught RuntimeException: " + e.getMessage());
            throw new ServiceCatalogueException();
        }

        log.debug("Capacity2FacadeREST.getCapacityByHsaId() EXIT");
        return GetCapacityByHsaIdResponse.jsonOK(result);
    }

    @Override
    public DeleteCapacityByHsaIdByProductByCodeResponse deleteCapacityByHsaIdByProductByCode(@PathParam("product") String product, @PathParam("code") String code, @PathParam("hsaId") String hsaId) throws Exception {
        log.debug("Delete: " + hsaId + ", " + product + ", " + code);
        try {
            capacityEjb.deleteByHsaIDAndProductAndCapacity(hsaId, product, code);
        } catch (RuntimeException e) {
            log.error(code);
            throw new ServiceCatalogueException();
        }

        return DeleteCapacityByHsaIdByProductByCodeResponse.oK();
    }

    @Override
    public GetCapacityFindexpiringByWithinDaysFromNowResponse getCapacityFindexpiringByWithinDaysFromNow(@PathParam("withinDaysFromNow") Long withinDaysFromNow) throws Exception {
        log.debug("CapacityFacadeREST.findExpiring(): ENTER");
        log.debug("CapacityFacadeREST.findExpiring() : daysFromNow = {}", withinDaysFromNow);

        if ((withinDaysFromNow == null) || (withinDaysFromNow.intValue() < 1) || (withinDaysFromNow.intValue() > 31)) {
            throw new ValidationException(ServiceErrorCode.INVALID_DAYS_FROM_NOW_RANGE, withinDaysFromNow, 1, 31);
        }

        UnitIds result = new UnitIds();
        try {
            Set<String> unitSet = notificationSupportEjb.findExpiring(withinDaysFromNow.intValue());
            List<String> unitList = new ArrayList<>(unitSet);
            result.setValues(unitList);

        } catch (RuntimeException e) {
            log.error("CapacityFacadeREST.findExpiring() Caught RuntimeException: " + e.getMessage());
            throw new ServiceCatalogueException();
        }

        log.debug("CapacityFacadeREST.findExpiring(): EXIT");
        return GetCapacityFindexpiringByWithinDaysFromNowResponse.jsonOK(result);
    }

    /**
     * Copies a full {@link CareUnitReferenceEntity} list to a list of
     * the Rest Model format {@link CareUnit} objects.
     *
     * @param entityList The entity to copy from
     * @return The populated List of {@link CareUnit} objects.
     */
    private List<CareUnitRefList> copyCareUnitFromEntityToModel(List<CareUnitReferenceEntity> entityList) {
        List<CareUnitRefList> response = new ArrayList<>();
        for (CareUnitReferenceEntity cure : entityList) {
            response.add(getCareUnitModelFromEntity(cure));
        }

        // return entityList.stream().map(this::getCareUnitModelFromEntity).collect(Collectors.toList());
        return response;
    }

    /**
     * Copies a single {@link CareUnitReferenceEntity} to the Rest Model format
     * {@link CareUnit} object.s
     *
     * @param cure The entity to copy from.
     * @return the populated {@link CareUnit} object.
     */
    private CareUnitRefList getCareUnitModelFromEntity(CareUnitReferenceEntity cure) {
        CareUnitRefList currentCareUnit = new CareUnitRefList();
        currentCareUnit.setHsaID(cure.getHsaID());

        // Products
        List<Product> products = new ArrayList<>();
        for (ProductReferenceEntity product : cure.getProduct()) {
            products.add(getProductModelFromEntity(product));
        }
        currentCareUnit.setProduct(products);

        // currentCareUnit.setProducts(cure.getProduct().stream().map(this::getProductModelFromEntity).collect(Collectors.toList()));
        return currentCareUnit;
    }

    /**
     * Copies a single {@link ProductReferenceEntity} to the Rest Model format
     * {@link Product} object.
     *
     * @param product The entity to copy from.
     * @return the populated {@link Product} object.
     */
    private Product getProductModelFromEntity(ProductReferenceEntity product) {
        Product currentProduct = new Product();
        currentProduct.setHsaID((product.getHsaID() == null) ? "" : product.getHsaID().getHsaID());
        currentProduct.setName(product.getName());

        // Capacities
        List<Capacity> capacities = new ArrayList<>();
        for (CapacityEntity ce : product.getCapacity()) {
            capacities.add(getCapacityModelFromEntity(ce));
        }
        currentProduct.setCapacity(capacities);

        //currentProduct.setCapacities(product.getCapacity().stream().map(this::getCapacityModelFromEntity).collect(Collectors.toList()));
        return currentProduct;
    }

    /**
     * Copies a single {@link CapacityEntity} to the Rest Model format
     * {@link Capacity} object.
     *
     * @param ce The entity to copy from.
     * @return the populated {@link Capacity} object.
     */
    private Capacity getCapacityModelFromEntity(CapacityEntity ce) {
        Capacity currentCapacity = new Capacity();
        currentCapacity.withValue(ce.getValue())
                .withValidTo(ce.getValidTo())
                .withCreationTime(ce.getCreationTime())
                .withProduct((ce.getProduct() == null) ? "" : ce.getProduct().getName())
                .withCode(ce.getCode())
                .withCodeSystem(ce.getCodeSystem())
                .withCreatedByHsaID(ce.getCreatedByHsaID())
                .withSourceSystemHsaID(ce.getSourceSystemHsaID());

        return currentCapacity;
    }

    /**
     * Find CareUnit for the given hsaID, if not found it will be created.
     *
     * @param hsaID
     * @return
     */
    private CareUnitReferenceEntity getCareUnitReference(String hsaID) {
        CareUnitReferenceEntity cre = capacityEjb.findCareUnitReference(hsaID);
        if (cre == null) {
            cre = new CareUnitReferenceEntity();
            cre.setHsaID(hsaID);
            capacityEjb.update(cre);
        }
        return cre;
    }

    /**
     * Searches all products referenced to the given CareUnit for a given
     * product, if the given product is not found it will be created and
     * returned.
     *
     * @param cre
     * @param product
     * @return
     */
    private ProductReferenceEntity getProductReferenceEntity(CareUnitReferenceEntity cre, String product) {
        if (cre.getProduct() != null) {
            for (ProductReferenceEntity pre : cre.getProduct()) {
                if (pre.getName().equals(product)) {
                    return pre;
                }
            }
        } else {
            Set<ProductReferenceEntity> preList = new HashSet<ProductReferenceEntity>();
            cre.setProduct(preList);
        }
        ProductReferenceEntity pre = new ProductReferenceEntity();
        pre.setName(product);
        pre.setCapacity(new HashSet<CapacityEntity>());
        capacityEjb.update(pre);
        cre.getProduct().add(pre);
        capacityEjb.update(cre);
        return pre;
    }
}
